home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / xulrunner / python / downloader.py < prev    next >
Encoding:
Python Source  |  2008-01-10  |  22.9 KB  |  660 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. from base64 import b64encode
  19. from gtcache import gettext as _
  20. from threading import RLock
  21. import os
  22. import re
  23. import shutil
  24.  
  25. from database import DDBObject, defaultDatabase
  26. from dl_daemon import daemon, command
  27. from download_utils import nextFreeFilename, getFileURLPath, filterDirectoryName
  28. from util import getTorrentInfoHash, returnsUnicode, checkU, returnsFilename, unicodify, checkF, stringify
  29. from platformutils import FilenameType
  30. import app
  31. import config
  32. import httpclient
  33. import indexes
  34. import prefs
  35. import random
  36. import views
  37. import platformutils
  38. import flashscraper
  39. import logging
  40. import traceback
  41. import templatehelper
  42. import fileutil
  43.  
  44. # a hash of download ids that the server knows about.
  45. _downloads = {}
  46.  
  47. # Returns an HTTP auth object corresponding to the given host, path or
  48. # None if it doesn't exist
  49. def findHTTPAuth(host,path,realm = None,scheme = None):
  50.     checkU(host)
  51.     checkU(path)
  52.     if realm:
  53.         checkU(realm)
  54.     if scheme:
  55.         checkU(scheme)
  56.     #print "Trying to find HTTPAuth with host %s, path %s, realm %s, and scheme %s" %(host,path,realm,scheme)
  57.     defaultDatabase.confirmDBThread()
  58.     for obj in views.httpauths:
  59.         if (obj.host == host and path.startswith(obj.path) and
  60.             (realm is None or obj.realm == realm) and
  61.             (scheme is None or obj.authScheme == scheme)):
  62.             return obj
  63.     return None
  64.  
  65.  
  66. class HTTPAuthPassword(DDBObject):
  67.     def __init__(self,username,password,host, realm, path, authScheme=u"Basic"):
  68.         checkU(username)
  69.         checkU(password)
  70.         checkU(host)
  71.         checkU(realm)
  72.         checkU(path)
  73.         checkU(authScheme)
  74.         oldAuth = findHTTPAuth(host,path,realm,authScheme)
  75.         while not oldAuth is None:
  76.             oldAuth.remove()
  77.             oldAuth = findHTTPAuth(host,path,realm,authScheme)
  78.         self.username = username
  79.         self.password = password
  80.         self.host = host
  81.         self.realm = realm
  82.         self.path = os.path.dirname(path)
  83.         self.authScheme = authScheme
  84.         DDBObject.__init__(self)
  85.  
  86.     def getAuthToken(self):
  87.         authString = u':'
  88.         self.confirmDBThread()
  89.         authString = self.username+u':'+self.password
  90.         return b64encode(authString)
  91.  
  92.     def getAuthScheme(self):
  93.         self.confirmDBThread()
  94.         return self.authScheme
  95.  
  96. totalUpRate = 0
  97. totalDownRate = 0
  98.  
  99. def _getDownloader (dlid):
  100.     return views.remoteDownloads.getItemWithIndex(indexes.downloadsByDLID, dlid)
  101.  
  102. @returnsUnicode
  103. def generateDownloadID():
  104.     dlid = u"download%08d" % random.randint(0,99999999)
  105.     while _getDownloader (dlid=dlid):
  106.         dlid = u"download%08d" % random.randint(0,99999999)
  107.     return dlid
  108.  
  109. class RemoteDownloader(DDBObject):
  110.     """Download a file using the downloader daemon."""
  111.  
  112.     def __init__(self, url, item, contentType = None, channelName = None):
  113.         checkU(url)
  114.         if contentType:
  115.             checkU(contentType)
  116.         self.origURL = self.url = url
  117.         self.itemList = [item]
  118.         self.dlid = generateDownloadID()
  119.         self.status = {}
  120.         if contentType is None:
  121.             # HACK:  Some servers report the wrong content-type for torrent
  122.             # files.  We try to work around that by assuming if the enclosure
  123.             # states that something is a torrent, it's a torrent.
  124.             # Thanks to j@v2v.cc
  125.             enclosureContentType = item.getFirstVideoEnclosureType()
  126.             if enclosureContentType == u'application/x-bittorrent':
  127.                 contentType = enclosureContentType
  128.         self.contentType = u""
  129.         self.deleteFiles = True
  130.         self.channelName = channelName
  131.         self.manualUpload = False
  132.         DDBObject.__init__(self)
  133.         if contentType is None:
  134.             self.contentType = u""
  135.         else:
  136.             self.contentType = contentType
  137.  
  138.         if self.contentType == u'':
  139.             self.getContentType()
  140.         else:
  141.             self.runDownloader()
  142.  
  143.     def signalChange (self, needsSave=True, needsSignalItem=True):
  144.         if needsSignalItem:
  145.             for item in self.itemList:
  146.                 item.signalChange(needsSave=False)
  147.         DDBObject.signalChange (self, needsSave=needsSave)
  148.  
  149.     def onContentType (self, info):
  150.         if not self.idExists():
  151.             return
  152.  
  153.         if info['status'] == 200:
  154.             self.url = info['updated-url'].decode('ascii','replace')
  155.             self.contentType = None
  156.             try:
  157.                 self.contentType = info['content-type'].decode('ascii','replace')
  158.             except:
  159.                 self.contentType = None
  160.             self.runDownloader()
  161.         else:
  162.             error = httpclient.UnexpectedStatusCode(info['status'])
  163.             self.onContentTypeError(error)
  164.  
  165.     def onContentTypeError (self, error):
  166.         if not self.idExists():
  167.             return
  168.  
  169.         self.status['state'] = u"failed"
  170.         self.status['shortReasonFailed'] = error.getFriendlyDescription()
  171.         self.status['reasonFailed'] = error.getLongDescription()
  172.         self.signalChange()
  173.  
  174.     def getContentType(self):
  175.         httpclient.grabHeaders(self.url, self.onContentType, self.onContentTypeError)
  176.  
  177.     @classmethod
  178.     def initializeDaemon(cls):
  179.         RemoteDownloader.dldaemon = daemon.ControllerDaemon()
  180.  
  181.     def _getRates(self):
  182.         state = self.getState()
  183.         if state == u'downloading':
  184.             return (self.status.get('rate', 0), self.status.get('upRate', 0))
  185.         if state == u'uploading':
  186.             return (0, self.status.get('upRate', 0))
  187.         return (0, 0)
  188.  
  189.     def beforeChangingStatus(self):
  190.         global totalDownRate
  191.         global totalUpRate
  192.         rates = self._getRates()
  193.         totalDownRate -= rates[0]
  194.         totalUpRate -= rates[1]
  195.  
  196.     def afterChangingStatus(self):
  197.         global totalDownRate
  198.         global totalUpRate
  199.         rates = self._getRates()
  200.         totalDownRate += rates[0]
  201.         totalUpRate += rates[1]
  202.  
  203.     @classmethod
  204.     def updateStatus(cls, data):
  205.         for field in data:
  206.             if field not in ['filename','shortFilename','channelName','metainfo','fastResumeData']:
  207.                 data[field] = unicodify(data[field])
  208.         self = _getDownloader (dlid=data['dlid'])
  209.         # print data
  210.         if self is not None:
  211.             try:
  212.                 if self.status == data:
  213.                     return
  214.             except Exception, e:
  215.                 # This is a known bug with the way we used to save fast resume
  216.                 # data
  217.                 print "WARNING exception when comparing status: %s" % e
  218.  
  219.             wasFinished = self.isFinished()
  220.             self.beforeChangingStatus()
  221.  
  222.             # FIXME: how do we get all of the possible bit torrent
  223.             # activity strings into gettext? --NN
  224.             if data.has_key('activity') and data['activity']:
  225.                 data['activity'] = _(data['activity'])
  226.  
  227.             self.status = data
  228.  
  229.             # Store the time the download finished
  230.             finished = self.isFinished() and not wasFinished
  231.             self.afterChangingStatus()
  232.  
  233.             if self.getState() == u'uploading' and not self.manualUpload and self.getUploadRatio() > 1.5:
  234.                 self.stopUpload()
  235.  
  236.             self.signalChange(needsSignalItem=not finished)
  237.             if finished:
  238.                 for item in self.itemList:
  239.                     item.onDownloadFinished()
  240.  
  241.     ##
  242.     # This is the actual download thread.
  243.     def runDownloader(self):
  244.         flashscraper.tryScrapingURL(self.url, self._runDownloader)
  245.  
  246.     def _runDownloader(self, url, contentType = None):
  247.         if not self.idExists():
  248.             return # we got deleted while we were doing the flash scraping
  249.         if contentType is not None:
  250.             self.contentType = contentType
  251.         if url is not None:
  252.             self.url = url
  253.             c = command.StartNewDownloadCommand(RemoteDownloader.dldaemon,
  254.                                                 self.url, self.dlid, self.contentType, self.channelName)
  255.             c.send()
  256.             _downloads[self.dlid] = self
  257.         else:
  258.             self.status["state"] = u'failed'
  259.             self.status["shortReasonFailed"] = _('File not found')
  260.             self.status["reasonFailed"] = _('Flash URL Scraping Error')
  261.         self.signalChange()
  262.  
  263.     ##
  264.     # Pauses the download.
  265.     def pause(self, block=False):
  266.         if _downloads.has_key(self.dlid):
  267.             c = command.PauseDownloadCommand(RemoteDownloader.dldaemon,
  268.                                              self.dlid)
  269.             c.send()
  270.         else:
  271.             self.beforeChangingStatus()
  272.             self.status["state"] = u"paused"
  273.             self.afterChangingStatus()
  274.             self.signalChange()
  275.  
  276.     ##
  277.     # Stops the download and removes the partially downloaded
  278.     # file.
  279.     def stop(self, delete):
  280.         if ((self.getState() in [u'downloading',u'uploading', u'paused'])):
  281.             if _downloads.has_key(self.dlid):
  282.                 c = command.StopDownloadCommand(RemoteDownloader.dldaemon,
  283.                                                 self.dlid, delete)
  284.                 c.send()
  285.                 del _downloads[self.dlid]
  286.         else:
  287.             if delete:
  288.                 self.delete()
  289.             self.status["state"] = u"stopped"
  290.             self.signalChange()
  291.  
  292.     def delete(self):
  293.         try:
  294.             filename = self.status['filename']
  295.         except KeyError:
  296.             return
  297.         try:
  298.             fileutil.delete(filename)
  299.         except:
  300.             logging.warn("Error deleting downloaded file: %s\n%s" % 
  301.                     (templatehelper.toUni(stringify(filename)), traceback.format_exc()))
  302.  
  303.         parent = os.path.join(filename, os.path.pardir)
  304.         parent = os.path.normpath(parent)
  305.         moviesDir = config.get(prefs.MOVIES_DIRECTORY)
  306.         if (os.path.exists(parent) and os.path.exists(moviesDir) and
  307.             not platformutils.samefile(parent, moviesDir) and
  308.             len(os.listdir(parent)) == 0):
  309.             try:
  310.                 os.rmdir(parent)
  311.             except:
  312.                 logging.warn("Error deleting empty download directory: %s\n%s" %
  313.                         (templatehelper.toUni(parent), traceback.format_exc()))
  314.  
  315.     ##
  316.     # Continues a paused, stopped, or failed download thread
  317.     def start(self):
  318.         if self.getState() == u'failed':
  319.             if _downloads.has_key (self.dlid):
  320.                 del _downloads[self.dlid]
  321.             self.dlid = generateDownloadID()
  322.             views.remoteDownloads.recomputeIndex(indexes.downloadsByDLID)
  323.             self.beforeChangingStatus()
  324.             self.status = {}
  325.             self.afterChangingStatus()
  326.             if self.contentType == u"":
  327.                 self.getContentType()
  328.             else:
  329.                 self.runDownloader()
  330.             self.signalChange()
  331.         elif self.getState() in (u'stopped', u'paused', u'offline'):
  332.             if _downloads.has_key(self.dlid):
  333.                 c = command.StartDownloadCommand(RemoteDownloader.dldaemon,
  334.                                                  self.dlid)
  335.                 c.send()
  336.             else:
  337.                 self.status['state'] = u'downloading'
  338.                 self.restart()
  339.                 self.signalChange()
  340.  
  341.     def migrate(self, directory):
  342.         if _downloads.has_key(self.dlid):
  343.             c = command.MigrateDownloadCommand(RemoteDownloader.dldaemon,
  344.                                                self.dlid, directory)
  345.             c.send()
  346.         else:
  347.             # downloader doesn't have our dlid.  Move the file ourself.
  348.             try:
  349.                 shortFilename = self.status['shortFilename']
  350.             except KeyError:
  351.                 print """\
  352. WARNING: can't migrate download because we don't have a shortFilename!
  353. URL was %s""" % self.url
  354.                 return
  355.             try:
  356.                 filename = self.status['filename']
  357.             except KeyError:
  358.                 print """\
  359. WARNING: can't migrate download because we don't have a filename!
  360. URL was %s""" % self.url
  361.                 return
  362.             if os.path.exists(filename):
  363.                 if 'channelName' in self.status and self.status['channelName'] is not None:
  364.                     channelName = filterDirectoryName(self.status['channelName'])
  365.                     directory = os.path.join (directory, channelName)
  366.                 try:
  367.                     os.makedirs(directory)
  368.                 except:
  369.                     pass
  370.                 newfilename = os.path.join(directory, shortFilename)
  371.                 if newfilename == filename:
  372.                     return
  373.                 newfilename = nextFreeFilename(newfilename)
  374.                 def callback():
  375.                     self.status['filename'] = newfilename
  376.                     self.signalChange()
  377.                 fileutil.migrate_file(filename, newfilename, callback)
  378.         for i in self.itemList:
  379.             i.migrateChildren(directory)
  380.  
  381.     def setDeleteFiles(self, deleteFiles):
  382.         self.deleteFiles = deleteFiles
  383.  
  384.     def setChannelName(self, channelName):
  385.         if self.channelName is None:
  386.             if channelName:
  387.                 checkF(channelName)
  388.             self.channelName = channelName
  389.  
  390.     ##
  391.     # Removes downloader from the database and deletes the file.
  392.     def remove(self):
  393.         global totalDownRate
  394.         global totalUpRate
  395.         rates = self._getRates()
  396.         totalDownRate -= rates[0]
  397.         totalUpRate -= rates[1]
  398.         self.stop(self.deleteFiles)
  399.         DDBObject.remove(self)
  400.  
  401.     def getType(self):
  402.         """Get the type of download.  Will return either "http" or
  403.         "bittorrent".
  404.         """
  405.         self.confirmDBThread()
  406.         if self.contentType == u'application/x-bittorrent':
  407.             return u"bittorrent"
  408.         else:
  409.             return u"http"
  410.  
  411.     ##
  412.     # In case multiple downloaders are getting the same file, we can support
  413.     # multiple items
  414.     def addItem(self,item):
  415.         if item not in self.itemList:
  416.             self.itemList.append(item)
  417.  
  418.     def removeItem(self, item):
  419.         self.itemList.remove(item)
  420.         if len (self.itemList) == 0:
  421.             self.remove()
  422.  
  423.     def getRate(self):
  424.         self.confirmDBThread()
  425.         return self.status.get('rate', 0)
  426.  
  427.     def getETA(self):
  428.         self.confirmDBThread()
  429.         return self.status.get('eta', 0)
  430.  
  431.     @returnsUnicode
  432.     def getStartupActivity(self):
  433.         self.confirmDBThread()
  434.         activity = self.status.get('activity')
  435.         if activity is None:
  436.             return _("starting up")
  437.         else:
  438.             return activity
  439.  
  440.     ##
  441.     # Returns the reason for the failure of this download
  442.     # This should only be called when the download is in the failed state
  443.     @returnsUnicode
  444.     def getReasonFailed(self):
  445.         if not self.getState() == u'failed':
  446.             msg = u"getReasonFailed() called on a non-failed downloader"
  447.             raise ValueError(msg)
  448.         self.confirmDBThread()
  449.         return self.status['reasonFailed']
  450.  
  451.     @returnsUnicode
  452.     def getShortReasonFailed(self):
  453.         if not self.getState() == u'failed':
  454.             msg = u"getShortReasonFailed() called on a non-failed downloader"
  455.             raise ValueError(msg)
  456.         self.confirmDBThread()
  457.         return self.status['shortReasonFailed']
  458.     ##
  459.     # Returns the URL we're downloading
  460.     @returnsUnicode
  461.     def getURL(self):
  462.         self.confirmDBThread()
  463.         return self.url
  464.  
  465.     ##    
  466.     # Returns the state of the download: downloading, paused, stopped,
  467.     # failed, or finished
  468.     @returnsUnicode    
  469.     def getState(self):
  470.         self.confirmDBThread()
  471.         return self.status.get('state', u'downloading')
  472.  
  473.     def isFinished(self):
  474.         return self.getState() in (u'finished', u'uploading')
  475.  
  476.     ##
  477.     # Returns the total size of the download in bytes
  478.     def getTotalSize(self):
  479.         self.confirmDBThread()
  480.         return self.status.get(u'totalSize', -1)
  481.  
  482.     ##
  483.     # Returns the current amount downloaded in bytes
  484.     def getCurrentSize(self):
  485.         self.confirmDBThread()
  486.         return self.status.get(u'currentSize', 0)
  487.  
  488.     ##
  489.     # Returns the filename that we're downloading to. Should not be
  490.     # called until state is "finished."
  491.     @returnsFilename
  492.     def getFilename(self):
  493.         self.confirmDBThread()
  494.         return self.status.get('filename', FilenameType(''))
  495.  
  496.     def onRestore(self):
  497.         self.deleteFiles = True
  498.         self.itemList = []
  499.         if self.dlid == 'noid':
  500.             # this won't happen nowadays, but it can for old databases
  501.             self.dlid = generateDownloadID()
  502.         self.status['rate'] = 0
  503.         self.status['upRate'] = 0
  504.         self.status['eta'] = 0
  505.  
  506.     def getUploadRatio(self):
  507.         size = self.getCurrentSize()
  508.         if size == 0:
  509.             return 0
  510.         return self.status.get('uploaded', 0) / size
  511.     
  512.     def restartIfNeeded(self):
  513.         if self.getState() in (u'downloading',u'offline'):
  514.             self.restart()
  515.         if self.getState() in (u'uploading'):
  516.             if self.manualUpload or self.getUploadRatio() < 1.5:
  517.                 self.restart()
  518.             else:
  519.                 self.stopUpload()
  520.  
  521.     def restart(self):
  522.         if len(self.status) == 0 or self.status.get('dlerType') is None:
  523.             if self.contentType == u"":
  524.                 self.getContentType()
  525.             else:
  526.                 self.runDownloader()
  527.         else:
  528.             _downloads[self.dlid] = self
  529.             c = command.RestoreDownloaderCommand(RemoteDownloader.dldaemon, 
  530.                                                  self.status)
  531.             c.send()
  532.  
  533.     def startUpload(self):
  534.         if self.getState() != u'finished' or self.getType() != u'bittorrent':
  535.             return
  536.         self.manualUpload = True
  537.         if _downloads.has_key(self.dlid):
  538.             c = command.StartDownloadCommand(RemoteDownloader.dldaemon,
  539.                                              self.dlid)
  540.             c.send()
  541.         else:
  542.             self.beforeChangingStatus()
  543.             self.status['state'] = u'uploading'
  544.             self.afterChangingStatus()
  545.             self.restart()
  546.             self.signalChange()
  547.  
  548.     def stopUpload(self):
  549.         if self.getState() != u"uploading":
  550.             return
  551.         if _downloads.has_key(self.dlid):
  552.             c = command.StopUploadCommand(RemoteDownloader.dldaemon,
  553.                                           self.dlid)
  554.             c.send()
  555.             del _downloads[self.dlid]
  556.         self.beforeChangingStatus()
  557.         self.status["state"] = u"finished"
  558.         self.afterChangingStatus()
  559.         self.signalChange()
  560.  
  561. def cleanupIncompleteDownloads():
  562.     downloadDir = os.path.join(config.get(prefs.MOVIES_DIRECTORY),
  563.             'Incomplete Downloads')
  564.     if not os.path.exists(downloadDir):
  565.         return
  566.  
  567.     filesInUse = set()
  568.     views.remoteDownloads.confirmDBThread()
  569.     for downloader in views.remoteDownloads:
  570.         if downloader.getState() in ('downloading', 'paused', 'offline'):
  571.             filename = downloader.getFilename()
  572.             if len(filename) > 0:
  573.                 if not os.path.isabs(filename):
  574.                     filename = os.path.join(downloadDir, filename)
  575.                 filesInUse.add(filename)
  576.  
  577.     for f in os.listdir(downloadDir):
  578.         f = os.path.join(downloadDir, f)
  579.         if f not in filesInUse:
  580.             try:
  581.                 if os.path.isfile(f):
  582.                     os.remove (f)
  583.                 elif os.path.isdir(f):
  584.                     shutil.rmtree (f)
  585.             except:
  586.                 # FIXME - maybe a permissions error?
  587.                 pass
  588.  
  589. def restartDownloads():
  590.     views.remoteDownloads.confirmDBThread()
  591.     for downloader in views.remoteDownloads:
  592.         downloader.restartIfNeeded()
  593.  
  594. def killUploaders(*args):
  595.     torrent_limit = config.get(prefs.UPSTREAM_TORRENT_LIMIT)
  596.     while (views.autoUploads.len() > torrent_limit):
  597.         views.autoUploads[0].stopUpload()
  598.  
  599. def configChangeUploaders(key, value):
  600.     if key == prefs.UPSTREAM_TORRENT_LIMIT.key:
  601.         killUploaders()
  602.  
  603. def limitUploaders():
  604.     views.autoUploads.addAddCallback(killUploaders)
  605.     config.addChangeCallback(configChangeUploaders)
  606.     killUploaders()
  607.         
  608.  
  609. def startupDownloader():
  610.     """Initialize the downloaders.
  611.  
  612.     This method currently does 2 things.  It deletes any stale files self in
  613.     Incomplete Downloads, then it restarts downloads that have been restored
  614.     from the database.  It must be called before any RemoteDownloader objects
  615.     get created.
  616.     """
  617.  
  618.     cleanupIncompleteDownloads()
  619.     RemoteDownloader.initializeDaemon()
  620.     limitUploaders()
  621.     restartDownloads()
  622.  
  623. def shutdownDownloader(callback = None):
  624.     if hasattr(RemoteDownloader, 'dldaemon') and RemoteDownloader.dldaemon is not None:
  625.         RemoteDownloader.dldaemon.shutdownDownloaderDaemon(callback=callback)
  626.  
  627. def lookupDownloader(url):
  628.     return views.remoteDownloads.getItemWithIndex(indexes.downloadsByURL, url)
  629.  
  630. def getExistingDownloaderByURL(url):
  631.     downloader = lookupDownloader(url)
  632.     return downloader
  633.  
  634. def getExistingDownloader(item):
  635.     downloader = lookupDownloader(item.getURL())
  636.     if downloader:
  637.         downloader.addItem(item)
  638.     return downloader
  639.  
  640. def getDownloader(item):
  641.     existing = getExistingDownloader(item)
  642.     if existing:
  643.         return existing
  644.     url = item.getURL()
  645.     channelName = platformutils.unicodeToFilename(item.getChannelTitle(True))
  646.     if not channelName:
  647.         channelName = None
  648.     if url.startswith(u'file://'):
  649.         path = getFileURLPath(url)
  650.         try:
  651.             getTorrentInfoHash(path)
  652.         except ValueError:
  653.             raise ValueError("Don't know how to handle %s" % url)
  654.         except IOError:
  655.             return None
  656.         else:
  657.             return RemoteDownloader(url, item, u'application/x-bittorrent', channelName=channelName)
  658.     else:
  659.         return RemoteDownloader(url, item, channelName=channelName)
  660.